﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace AstoriaOverAstoria
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq.Expressions;
    using System.Reflection;

    /// <summary><see cref="IQueryResultsProcessor"/> which turns ExpandedWrapper instances
    /// into new ExpandedWrapper instances which can be consumed by the server.</summary>
    /// <remarks>This class servers several purposes:
    ///    - Has public methods to create the description according to which the results should be transformed.
    ///    - Stores the definition of the transformation of the results.
    ///    - Before processing it compiles the transformation into a function which is then used (for performance reasons)
    ///    - It implements the <see cref="IQueryResultsProcessor"/> and is called to process the results of the query.
    /// </remarks>
    internal class ExpandedWrapperQueryResultsProcessor : IQueryResultsProcessor
    {
        /// <summary>List of expanded properties.</summary>
        private List<ExpandedProperty> expandedProperties;

        /// <summary>The type of the expanded wrapper to be returned to the server.</summary>
        private Type expandedWrapperType;

        /// <summary>Parameter expression which represents the source element (also an ExpandedWrapper but of a different type).</summary>
        private ParameterExpression sourceElementParameter;

        /// <summary>Parameter expression which represents the <see cref="AstoriaOverAstoriaContext"/> the query is executing on.</summary>
        private ParameterExpression contextParameter;

        /// <summary>Query results processor for the ExpandedElement value.</summary>
        private IQueryResultsProcessor expandedElementResultProcessor;

        /// <summary>Cached compiled function which processes the results.</summary>
        private Func<object, AstoriaOverAstoriaContext, object> processSingleResult;

        /// <summary>Creates new query results processor.</summary>
        /// <param name="sourceElementType">The type of the source element (the type returned by the client query) (expanded wraper always).</param>
        /// <param name="expandedWrapperType">The type of the results to return. (expanded wrapper always).</param>
        /// <param name="expandedElementResultProcessor">The query results processor for the ExpandedElement value.</param>
        public ExpandedWrapperQueryResultsProcessor(Type sourceElementType, Type expandedWrapperType, IQueryResultsProcessor expandedElementResultProcessor)
        {
            Debug.Assert(sourceElementType != null, "sourceElementType != null");
            Debug.Assert(expandedWrapperType != null, "expandedWrapperType != null");
            Debug.Assert(TypeSystem.IsExpandedWrapperType(sourceElementType), "The source element type should be an ExpandedWrapper");
            Debug.Assert(TypeSystem.IsExpandedWrapperType(expandedWrapperType), "The expanded wrapper type should be an ExpandedWrapper");

            this.expandedProperties = new List<ExpandedProperty>();
            this.expandedWrapperType = expandedWrapperType;
            this.expandedElementResultProcessor = expandedElementResultProcessor;
            this.sourceElementParameter = Expression.Parameter(sourceElementType, "element");
            this.contextParameter = Expression.Parameter(typeof(AstoriaOverAstoriaContext), "context");
        }

        /// <summary>Adds an expanded property.</summary>
        /// <param name="projectedIndex">The index of the ProjectedProperty onto which this expanded property is projected.</param>
        /// <param name="queryResultsProcessor">The query results processor to process the results of the expanded property with.</param>
        public void AddExpandedProperty(int projectedIndex, IQueryResultsProcessor queryResultsProcessor)
        {
            Debug.Assert(projectedIndex >= 0 && projectedIndex < this.expandedWrapperType.GetGenericArguments().Length - 1, "The projected index is too small or too large.");

            this.expandedProperties.Add(new ExpandedProperty
            {
                ProjectedIndex = projectedIndex,
                QueryResultsProcessor = queryResultsProcessor
            });
        }

        #region IQueryResultsProcessor Members

        /// <summary>Method which processes a single result of a query before it is handed to the server.</summary>
        /// <param name="result">The result produced by the query (and any result processors) so far.</param>
        /// <param name="context">The context which is executing the query.</param>
        /// <returns>The result of the query after the transformation.</returns>
        object IQueryResultsProcessor.ProcessSingleResult(object result, AstoriaOverAstoriaContext context)
        {
            this.InitializeResultProcessing();
            return this.processSingleResult(result, context);
        }

        /// <summary>Method which processes the results of a query before they are handed to the server.</summary>
        /// <param name="results">The results of the query so far.</param>
        /// <param name="context">The context which is executing the query.</param>
        /// <returns>The results of the query after the transformation.</returns>
        System.Collections.IEnumerable IQueryResultsProcessor.ProcessResults(System.Collections.IEnumerable results, AstoriaOverAstoriaContext context)
        {
            this.InitializeResultProcessing();
            foreach (object result in results)
            {
                yield return this.processSingleResult(result, context);
            }
        }

        #endregion

        /// <summary>Initializes the function used to process the results.</summary>
        private void InitializeResultProcessing()
        {
            if (this.processSingleResult == null)
            {
                // It's much easier to construct the desired behavior using LINQ to Objects, then compile the lambda and run it
                //    then doing this using reflection. It's also much faster that way.
                // For more compilcated stuff we can call helper methods anyway.

                // Sort the properties by their projected index
                this.expandedProperties.Sort((x, y) => x.ProjectedIndex == y.ProjectedIndex ? 0 : (x.ProjectedIndex < y.ProjectedIndex ? -1 : 1));

                LambdaExpression lambda = Expression.Lambda(
                    this.CreateNewExpandedWrapperExpression(),
                    this.sourceElementParameter,
                    this.contextParameter);
                ParameterExpression untypedSourceElementParameter = Expression.Parameter(typeof(object), "untypedElement");
                lambda = Expression.Lambda(
                    Expression.Invoke(
                        lambda,
                        Expression.Convert(untypedSourceElementParameter, this.sourceElementParameter.Type),
                        this.contextParameter),
                    untypedSourceElementParameter,
                    this.contextParameter);
                this.processSingleResult = (Func<object, AstoriaOverAstoriaContext, object>)lambda.Compile();
            }
        }

        /// <summary>Creates expression tree which converts the results.</summary>
        /// <returns>Expression which evaluates to the ExpandedWrapper to be returned to the server.</returns>
        /// <remarks>This expression is compiled into a function used to process the results.</remarks>
        private Expression CreateNewExpandedWrapperExpression()
        {
            // As with ProjectedWrapper it's better to use LINQ to Objects to construct the desired result and compile that into IL
            // It's easier to implement (more type safety actually) and it's much better performance (for multiple resutls)

            // We need to bind all expanded properties plus ExpandedElement and Description
            MemberBinding[] bindings = new MemberBinding[this.expandedProperties.Count + 2];

            PropertyInfo expandedElementProperty = this.expandedWrapperType.GetProperty("ExpandedElement");

            // element.ExpandedElement
            Expression expandedElement = Expression.Property(this.sourceElementParameter, "ExpandedElement");
            if (this.expandedElementResultProcessor != null)
            {
                // (type)this.expandedElementResultProcessor.ProcessSingleResult((object)element.ExpandedElement, context)
                expandedElement = Expression.Convert(
                    Expression.Call(
                        Expression.Constant(this.expandedElementResultProcessor, typeof(IQueryResultsProcessor)),
                        typeof(IQueryResultsProcessor).GetMethod("ProcessSingleResult"),
                        Expression.Convert(expandedElement, typeof(object)),
                        this.contextParameter),
                    expandedElementProperty.PropertyType);
            }

            // ExpandedElement = ...
            bindings[0] = Expression.Bind(
                expandedElementProperty,
                expandedElement);

            // Description = element.Description
            bindings[1] = Expression.Bind(
                this.expandedWrapperType.GetProperty("Description"),
                Expression.Property(this.sourceElementParameter, "Description"));

            int bindingIndex = 2;
            foreach (var expandedProperty in this.expandedProperties)
            {
                string propertyName = TypeSystem.GetProjectedPropertyName(expandedProperty.ProjectedIndex);
                PropertyInfo expandedPropertyInfo = this.expandedWrapperType.GetProperty(propertyName);

                // element.ProjectedProperty#
                Expression projectedProperty = Expression.Property(this.sourceElementParameter, propertyName);
                if (expandedProperty.QueryResultsProcessor != null)
                {
                    Type propertyElementType = TypeSystem.GetIEnumerableElementType(expandedPropertyInfo.PropertyType);

                    if (propertyElementType == null)
                    {
                        // Entity reference (no IEnumerable)
                        // (type)expandedProperty.QueryResultsProcessor.ProcessSingleResult((object)element.ProjectedProperty#, context)
                        Expression callProcessSingleResult = Expression.Call(
                            Expression.Constant(expandedProperty.QueryResultsProcessor, typeof(IQueryResultsProcessor)),
                            typeof(IQueryResultsProcessor).GetMethod("ProcessSingleResult"),
                            Expression.Convert(projectedProperty, typeof(object)), 
                            this.contextParameter);
                        projectedProperty = Expression.Convert(
                            callProcessSingleResult,
                            expandedPropertyInfo.PropertyType);
                    }
                    else
                    {
                        // Entity set reference (IEnumerable)
                        // expandedProperty.QueryResultsProcessor.ProcessResults((IEnumerable)element.ProjectedProperty#, context).Cast<type>()
                        Expression callProcessResults = Expression.Call(
                            Expression.Constant(expandedProperty.QueryResultsProcessor, typeof(IQueryResultsProcessor)),
                            typeof(IQueryResultsProcessor).GetMethod("ProcessResults"),
                            Expression.Convert(projectedProperty, typeof(IEnumerable)),
                            this.contextParameter);
                        projectedProperty = Expression.Call(
                            typeof(System.Linq.Enumerable).GetMethod("Cast", BindingFlags.Static | BindingFlags.Public).MakeGenericMethod(propertyElementType),
                            callProcessResults);
                    }
                }

                // ProjectedProperty# = ...
                bindings[bindingIndex++] = Expression.Bind(
                    expandedPropertyInfo,
                    projectedProperty);
            }

            return Expression.MemberInit(
                Expression.New(this.expandedWrapperType),
                bindings);
        }

        /// <summary>Helper class to store expanded properties information.</summary>
        private class ExpandedProperty
        {
            /// <summary>The index of the ProjectedProperty this expanded property is projected onto.</summary>
            public int ProjectedIndex { get; set; }

            /// <summary>The query results processor to be applied to the results fo the property expansion.</summary>
            public IQueryResultsProcessor QueryResultsProcessor { get; set; }
        }
    }
}
